package com.asgard.crypto;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXCertPathChecker;
import java.security.cert.PKIXCertPathValidatorResult;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;

import javax.security.auth.x500.X500Principal;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.DisplayText;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.encoders.Hex;

/**
 * 该类包装了X509证书的常见操作。
 */
public class Certificate {

    public static final ASN1ObjectIdentifier ROLE = new ASN1ObjectIdentifier("1.2.6.1").intern();
    public static final ASN1ObjectIdentifier SUB_ROLE = new ASN1ObjectIdentifier("1.2.6.2").intern();
    public static final ASN1ObjectIdentifier MEMBER_ID = new ASN1ObjectIdentifier("1.2.6.3").intern();
    public static final ASN1ObjectIdentifier ISSUER_SN = new ASN1ObjectIdentifier("1.2.6.4").intern();
    public static final ASN1ObjectIdentifier ISSUER_MEMBER_ID = new ASN1ObjectIdentifier("1.2.6.5").intern();

    private final byte[] cerBytes;
    private final X509Certificate x509Cert;

    static final String PREFIX = "-----BEGIN CERTIFICATE-----\n";
    static final String SUFFIX = "\n-----END CERTIFICATE-----";

    static {
        // 初始化加密库
        CryptoUtils.init();
    }

    /**
     * 采用BASE64字符串初始化证书。
     *
     * @param str BASE64字符串
     * @throws CertificateException 证书格式不正确
     */
    public Certificate(String str) throws CertificateException {
        this(str.startsWith(PREFIX) ? str.getBytes() : (PREFIX + str + SUFFIX).getBytes());
    }

    /**
     * 采用X509格式字节数组初始化证书。
     *
     * @param cerBytes X509格式字节数组
     * @throws CertificateException 证书格式不正确
     */
    public Certificate(byte[] cerBytes) throws CertificateException {
        X509Certificate x509Certificate = null;
        try (ByteArrayInputStream bin = new ByteArrayInputStream(cerBytes)) {
            CertificateFactory fact = CertificateFactory.getInstance("X.509", "BC");
            x509Certificate = (X509Certificate) fact.generateCertificate(bin);
        } catch (NoSuchProviderException e) {
            throw new RuntimeException("BouncyCastle is not loaded.", e);
        } catch (IOException e) {
            // cannot happen
        }
        this.x509Cert = x509Certificate;
        this.cerBytes = x509Cert.getEncoded();
    }

    /**
     * 使用X509格式证书初始化。
     *
     * @param x509Cert X509格式证书
     * @throws CertificateEncodingException 证书格式不正确
     */
    public Certificate(X509Certificate x509Cert) throws CertificateEncodingException {
        this.cerBytes = x509Cert.getEncoded();
        this.x509Cert = x509Cert;
    }

    /**
     * 通过文件构造证书。
     *
     * @param certFile 文件
     * @return 证书
     * @throws CertificateException 证书格式不正确
     * @throws IOException          输入输出异常
     */
    public static Certificate fromFile(File certFile) throws CertificateException, IOException {
        byte[] certBytes;
        try (FileInputStream fin = new FileInputStream(certFile);
                ByteArrayOutputStream bout = new ByteArrayOutputStream()) {
            IOUtils.copy(fin, bout);
            certBytes = bout.toByteArray();
        }
        return new Certificate(certBytes);
    }

    /**
     * 获取X509格式的证书。
     *
     * @return X509格式的证书
     */
    public X509Certificate getX509Cert() {
        return x509Cert;
    }

    /**
     * 获取X509字节数组表示的证书。
     *
     * @return X509字节数组表示的证书。
     */
    public byte[] getBytes() {
        return cerBytes;
    }

    /**
     * 校验证书日期。
     *
     * @throws CertificateExpiredException     证书已过期
     * @throws CertificateNotYetValidException 证书还未生效
     */
    public void checkDate() throws CertificateExpiredException, CertificateNotYetValidException {
        x509Cert.checkValidity();
    }

    /**
     * 校验证书日期，采用指定的时间戳。
     *
     * @param timestamp 时间戳
     * @throws CertificateExpiredException     证书已过期
     * @throws CertificateNotYetValidException 证书还未生效
     */
    public void checkDate(long timestamp) throws CertificateExpiredException, CertificateNotYetValidException {
        x509Cert.checkValidity(new Date(timestamp));
    }

    /**
     * 校验证书链，注意此方法不校验证书的撤销。
     *
     * @param timestamp    时间戳
     * @param caCert       信任的CA证书。
     * @param certificates 中间证书
     * @throws InvalidAlgorithmParameterException 证书算法参数无效
     * @throws NoSuchAlgorithmException           证书算法无效
     * @throws CertificateException               证书不正确
     * @throws CertPathValidatorException         证书路径不正确
     */
    public void validate(long timestamp, Certificate caCert, Certificate... certificates)
            throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, CertificateException,
            CertPathValidatorException {
        if (certificates == null || certificates.length == 0) {
            validate(timestamp, caCert.getX509Cert());
            return;
        }
        X509Certificate[] certs = new X509Certificate[certificates.length];
        for (int i = 0; i < certificates.length; i++)
            certs[i] = certificates[i].getX509Cert();
        validate(timestamp, caCert.getX509Cert(), certs);
    }

    /**
     * 校验自签名证书，注意此方法不校验证书的撤销。
     *
     * @param timestamp 时间戳
     * @throws InvalidAlgorithmParameterException 证书算法参数无效
     * @throws NoSuchAlgorithmException           证书算法无效
     * @throws CertificateException               证书不正确
     * @throws CertPathValidatorException         证书路径不正确
     */
    public void validateSelf(long timestamp) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException,
            CertificateException, CertPathValidatorException {
        validate(timestamp, x509Cert);
    }

    /**
     * 校验证书链，注意此方法不校验证书的撤销。
     *
     * @param timestamp    时间戳
     * @param caCert       信任的CA证书。
     * @param certificates 中间证书，顺序为从低到高
     * @throws InvalidAlgorithmParameterException 证书算法参数无效
     * @throws NoSuchAlgorithmException           证书算法无效
     * @throws CertificateException               证书不正确
     * @throws CertPathValidatorException         证书路径不正确
     */
    public PKIXCertPathValidatorResult validate(long timestamp, X509Certificate caCert, X509Certificate... certificates)
            throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, CertificateException,
            CertPathValidatorException {
        if (certificates == null || certificates.length == 0)
            certificates = new X509Certificate[] { x509Cert };
        else
            certificates = ArrayUtils.add(certificates, 0, x509Cert);
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509", "BC");
            CertPath cp = cf.generateCertPath(Arrays.asList(certificates));
            TrustAnchor trustAnchor = new TrustAnchor(caCert, null);
            CertPathValidator cpv = CertPathValidator.getInstance("PKIX", "BC");
            PKIXParameters pkixParams = new PKIXParameters(Collections.singleton(trustAnchor));
            pkixParams.setRevocationEnabled(false);
            pkixParams.addCertPathChecker(new CertChecker());
            pkixParams.setDate(new Date(timestamp));
            PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) cpv.validate(cp, pkixParams);
            return result;
        } catch (NoSuchProviderException e) {
            throw new RuntimeException("Bouncycastle not initialized. ");
        }
    }

    /**
     * 获取证书对应的ECKey。
     *
     * @return 证书对应的ECKey
     */
    public ECKey getECKey() {
        return new ECKey(getPublicKey());
    }

    /**
     * 判断证书角色是否是指定的角色。
     *
     * @param role 角色
     * @return 是否是指定的角色
     */
    public boolean isRole(String role) {
        return Objects.equals(getRole(), role);
    }

    /**
     * 判断证书的子角色是否是指定的角色。
     *
     * @param role    角色
     * @param subRole 子角色
     * @return 是否是指定的子角色
     */
    public boolean isRole(String role, String subRole) {
        return Objects.equals(getRole(), role) && Objects.equals(getSubRole(), subRole);
    }

    /**
     * 判断是否为ROOT证书。
     *
     * @return 是否为ROOT证书
     */
    public boolean isRoot() {
        return isRole("ROOT");
    }

    /**
     * 判断是否为VP证书。
     *
     * @return 是否为VP证书
     */
    public boolean isVP() {
        return isRole("CONNECTION", "VP");
    }

    /**
     * 判断是否为NVP证书。
     *
     * @return 是否为NVP证书
     */
    public boolean isNVP() {
        return isRole("CONNECTION", "NVP");
    }

    /**
     * 获取证书类型。
     *
     * @return 证书类型
     */
    public final String getType() {
        return x509Cert.getType();
    }

    /**
     * 获取证书版本号。
     *
     * @return 证书版本号
     */
    public int getVersion() {
        return x509Cert.getVersion();
    }

    /**
     * 获取证书序列号。
     *
     * @return 证书序列号
     */
    public BigInteger getSN() {
        return x509Cert.getSerialNumber();
    }

    /**
     * 获取证书中的公钥。
     *
     * @return 证书公钥
     */
    public PublicKey getPublicKey() {
        return x509Cert.getPublicKey();
    }

    /**
     * 获取证书中的签名。
     *
     * @return 证书签名
     */
    public byte[] getSignature() {
        return x509Cert.getSignature();
    }

    /**
     * 获取证书签名算法名称。
     *
     * @return 证书签名算法名称
     */
    public String getSigAlgName() {
        return x509Cert.getSigAlgName();
    }

    /**
     * 获取证书签发者。
     *
     * @return 证书签发者
     */
    public X500Principal getIssuerX500Principal() {
        return x509Cert.getIssuerX500Principal();
    }

    /**
     * 获取证书授予者。
     *
     * @return 证书授予者
     */
    public X500Principal getSubjectX500Principal() {
        return x509Cert.getSubjectX500Principal();
    }

    /**
     * 获取证书签发者名称。
     *
     * @return 证书签发者名称
     */
    public String getIssuerName() {
        return x509Cert.getIssuerX500Principal().getName();
    }

    /**
     * 获取证书授予者名称。
     *
     * @return 证书授予者名称
     */
    public String getSubjectName() {
        return x509Cert.getSubjectX500Principal().getName();
    }

    /**
     * 获取证书生效起始日期。
     *
     * @return 证书生效起始日期
     */
    public Date getNotBefore() {
        return x509Cert.getNotBefore();
    }

    /**
     * 获取证书效力终结日期。
     *
     * @return 证书效力终结日期
     */
    public Date getNotAfter() {
        return x509Cert.getNotAfter();
    }

    /**
     * 获取角色，即1.2.6.1扩展值。
     *
     * @return 角色
     */
    public String getRole() {
        byte[] bytes = x509Cert.getExtensionValue(ROLE.getId());
        return getValue(bytes);
    }

    /**
     * 获取子角色，即1.2.6.2扩展值。
     *
     * @return 子角色
     */
    public String getSubRole() {
        byte[] bytes = x509Cert.getExtensionValue(SUB_ROLE.getId());
        return getValue(bytes);
    }

    /**
     * 获取证书扩展字段MEMBER_ID，即1.2.6.3扩展值。
     *
     * @return 证书扩展字段MEMBER_ID
     */
    public String getMemberID() {
        byte[] bytes = x509Cert.getExtensionValue(MEMBER_ID.getId());
        return getValue(bytes);
    }

    /**
     * 获取颁发者SN，即1.2.6.4扩展值。
     *
     * @return 颁发者SN
     */
    public BigInteger getIssuerSN() {
        byte[] bytes = x509Cert.getExtensionValue(ISSUER_SN.getId());
        return new BigInteger(getValue(bytes));
    }

    /**
     * 获取颁发者MEMBER_ID，即1.2.6.5扩展值。
     *
     * @return 颁发者MEMBER ID
     */
    public String getIssuerMemberID() {
        byte[] bytes = x509Cert.getExtensionValue(ISSUER_MEMBER_ID.getId());
        return getValue(bytes);
    }

    private static String getValue(byte[] bytes) {
        try {
            ASN1OctetString dd = DEROctetString.getInstance(ASN1Primitive.fromByteArray(bytes));
            DisplayText text = DisplayText.getInstance(ASN1Primitive.fromByteArray(dd.getOctets()));
            return text.getString();
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public String toString() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        StringBuilder sb = new StringBuilder();
        sb.append("Begin Certificate").append("\n");
        sb.append("  Version: ").append("" + this.getVersion()).append("\n");
        sb.append("  Type: ").append("" + this.getType()).append("\n");
        sb.append("  MEMBER_ID: ").append("" + this.getMemberID()).append("\n");
        sb.append("  SN: ").append("" + this.getSN()).append("\n");
        sb.append("  Issuer_MEMBER_ID: ").append("" + this.getIssuerMemberID()).append("\n");
        sb.append("  Issuer_SN: ").append("" + this.getIssuerSN()).append("\n");
        sb.append("  IssuerName: ").append("" + this.getIssuerName()).append("\n");
        sb.append("  SigAlgName: ").append(this.getSigAlgName()).append("\n");
        sb.append("  SubjectName: ").append("" + this.getSubjectName()).append("\n");
        sb.append("  Role: ").append("" + this.getRole()).append("\n");
        sb.append("  Sub-Role: ").append("" + this.getSubRole()).append("\n");
        String[] pubKey = this.getPublicKey().toString().split("\n");
        sb.append("  PublicKey: ");
        for (int i = 0; i < pubKey.length; i++) {
            String p = pubKey[i].trim();
            if (p.isEmpty())
                continue;
            if (i > 0)
                sb.append("             ");
            sb.append(p).append("\n");
        }
        if (this.getPublicKey() instanceof BCECPublicKey) {
            sb.append("  PublicKeyHex: ");
            sb.append(Hex.toHexString(Elliptic.getBytesCompact(this.getPublicKey())));
            sb.append("\n");
        }
        this.getNotBefore().getTime();
        sb.append("  NotBefore: ").append(sdf.format(this.getNotBefore())).append("\n");
        sb.append("  NotAfter: ").append(sdf.format(this.getNotAfter())).append("\n");
        sb.append("End Certificate");
        return sb.toString();
    }

    /**
     * 获取Base64编码字符串。
     *
     * @return Base64编码字符串
     */
    public String getBase64String() {
        return PREFIX + getRawBase64String() + SUFFIX;
    }

    /**
     * 获取没有前缀和后缀的Base64编码字符串。
     *
     * @return Base64编码字符串
     */
    public String getRawBase64String() {
        return Base64.toBase64String(getBytes());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        Certificate that = (Certificate) o;
        return new EqualsBuilder().append(x509Cert, that.x509Cert).isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37).append(x509Cert).toHashCode();
    }

    /**
     * 用证书校验签名。
     *
     * @param data 数据
     * @param sig  签名
     * @return 签名校验是否成功
     * @throws CertificateException 证书异常
     */
    public boolean verify(byte[] data, byte[] sig) throws CertificateException {
        PublicKey pubKey = x509Cert.getPublicKey();
        try {
            Signature ecdsaVerify = Signature.getInstance(x509Cert.getSigAlgName(), "BC");
            ecdsaVerify.initVerify(pubKey);
            ecdsaVerify.update(data);
            return ecdsaVerify.verify(sig);
        } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
            throw new CertificateException(e);
        } catch (InvalidKeyException e) {
            throw new CertificateException(e);
        } catch (SignatureException e) {
            throw new CertificateException(e);
        }
    }

    /**
     * 证书扩展项
     */
    public static class CertChecker extends PKIXCertPathChecker {

        @Override
        public void init(boolean forward) throws CertPathValidatorException {
            // do nothing
        }

        @Override
        public boolean isForwardCheckingSupported() {
            return true;
        }

        @Override
        public Set<String> getSupportedExtensions() {
            Set<String> ext = new HashSet<>();
            ext.add(ROLE.getId());
            ext.add(SUB_ROLE.getId());
            ext.add(MEMBER_ID.getId());
            ext.add(ISSUER_SN.getId());
            ext.add(ISSUER_MEMBER_ID.getId());
            return ext;
        }

        @Override
        public void check(java.security.cert.Certificate cert, Collection<String> unresolvedCritExts)
                throws CertPathValidatorException {
            Iterator<String> it = unresolvedCritExts.iterator();
            while (it.hasNext()) {
                String t = it.next();
                if (t.equals(ROLE.getId()) || t.equals(SUB_ROLE.getId()) || t.equals(MEMBER_ID.getId())
                        || t.equals(ISSUER_SN.getId()) || t.equals(ISSUER_MEMBER_ID.getId()))
                    it.remove();
            }
        }

    }

}
